This repository has no description
1import { error, redirect } from "@sveltejs/kit";
2import { createBobbinClient } from "$lib/api/client";
3import { count } from "$lib/api/count";
4import { gitTarget, resolveDefaultBranch } from "$lib/api/gitclient";
5import { getStarRkey } from "$lib/api/graph";
6import { IdentityCache, resolveMiniDoc } from "$lib/api/identity";
7import { parallel, toHttpError } from "$lib/api/load";
8import { getRepo } from "$lib/api/records";
9import { repoNameOf, resolveRepoByName } from "$lib/api/repo";
10import { didFromUri, rkeyFromUri } from "$lib/api/uri";
11import type { BobbinContext } from "$lib/api/client";
12import type { RepoCounts, RepoInfo, RepoSource } from "$lib/components/repo/types";
13import type { LayoutLoad } from "./$types";
14
15const FALLBACK_BRANCH = "main";
16
17const resolveSource = async (
18 ctx: BobbinContext,
19 uri: string | undefined
20): Promise<RepoSource | null> => {
21 if (!uri?.startsWith("at://")) return null;
22 try {
23 const view = await getRepo(ctx, uri);
24 const ownerDid = didFromUri(view.uri);
25 const owner = await new IdentityCache(ctx).resolve(ownerDid).catch(() => null);
26 return { ownerHandle: owner?.handle ?? ownerDid, name: repoNameOf(view) };
27 } catch {
28 // a fork whose source is gone still renders, just without the attribution
29 return null;
30 }
31};
32
33// the layout reset also cuts us off from `[handle]/+layout.ts`, so identity gets
34// resolved again here
35export const load: LayoutLoad = async (event) => {
36 const parent = await event.parent();
37 const identifier = decodeURIComponent(event.params.handle);
38 const name = decodeURIComponent(event.params.repo);
39
40 // rejects bare words so unrelated paths 404 instead of resolving as actors
41 if (!identifier.startsWith("did:") && !identifier.includes(".")) {
42 error(404, "Not found");
43 }
44
45 const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch });
46 const doc = await resolveMiniDoc(ctx, identifier).catch((cause) =>
47 toHttpError(cause, "Could not resolve user")
48 );
49
50 // redirects dids and stale handles to the canonical handle url
51 const canonical = doc.handle && !doc.handle.endsWith(".invalid") ? doc.handle : null;
52 if (canonical && identifier.toLowerCase() !== canonical.toLowerCase()) {
53 redirect(307, `/${canonical}/${event.params.repo}${event.url.search}`);
54 }
55
56 const view = await resolveRepoByName(ctx, doc.did, name).catch((cause) =>
57 toHttpError(cause, "Could not load repository")
58 );
59 if (!view) error(404, `${doc.handle}/${name} does not exist`);
60
61 const record = view.value;
62 const repoDid = record.repoDid;
63 const viewerDid = parent.auth?.did;
64 const git = gitTarget(parent.publicConfig, { uri: view.uri, repoDid }, event.fetch);
65
66 const stats = await parallel({
67 // only the git backend knows the default branch, and one that is down
68 // or still syncing shouldn't take out the whole layout
69 defaultBranch: resolveDefaultBranch(git),
70 stars: repoDid
71 ? count(ctx, "sh.tangled.feed.countStars", repoDid).catch(() => null)
72 : Promise.resolve(null),
73 // the tabs count what is still open, closed ones matter on their own pages
74 issues: repoDid
75 ? count(ctx, "sh.tangled.repo.countIssues", repoDid, { state: "open" }).catch(() => null)
76 : Promise.resolve(null),
77 pulls: repoDid
78 ? count(ctx, "sh.tangled.repo.countPulls", repoDid, { status: "open" }).catch(() => null)
79 : Promise.resolve(null),
80 forks: repoDid
81 ? count(ctx, "sh.tangled.repo.countForks", repoDid).catch(() => null)
82 : Promise.resolve(null),
83 viewerStarRkey:
84 viewerDid && repoDid
85 ? getStarRkey(ctx, viewerDid, repoDid).catch(() => null)
86 : Promise.resolve(null),
87 source: resolveSource(ctx, record.source)
88 });
89
90 const repo: RepoInfo = {
91 uri: view.uri,
92 rkey: rkeyFromUri(view.uri),
93 name: repoNameOf(view),
94 ownerDid: doc.did,
95 ownerHandle: doc.handle,
96 repoDid,
97 knot: record.knot,
98 spindle: record.spindle,
99 description: record.description,
100 website: record.website,
101 topics: record.topics,
102 source: stats.source ?? undefined,
103 defaultBranch: stats.defaultBranch ?? FALLBACK_BRANCH
104 };
105
106 const counts: RepoCounts = {
107 stars: stats.stars?.count ?? 0,
108 issues: stats.issues?.count ?? 0,
109 pulls: stats.pulls?.count ?? 0,
110 forks: stats.forks?.count ?? 0
111 };
112
113 return { repo, counts, viewerStarRkey: stats.viewerStarRkey };
114};